using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using Commons.Utils;

namespace Commons.Injector
{
	public delegate void ScopeChangeDeletage(object sender, ScopeChangeEventArgs EventArgs);

	public class Context : IDisposable
	{
		public event ScopeChangeDeletage ScopeChanging;
		public event ScopeChangeDeletage ScopeChanged;

		public const string GlobalScope = "global";
		private string currentScope;

		public int NumInitSteps { get; protected set; }
		public int TotalInitSteps { get; protected set; }
		public int NumLoadSteps { get; protected set; }
		public int TotalLoadSteps { get; protected set; }
		public bool Initialized { get; protected set; }
		public string CurrentScope
		{
			get
			{
				return String.IsNullOrEmpty(currentScope) ? GlobalScope : currentScope;
			}
		}

		public Dictionary<string, ObjectDefinition> definitions = new Dictionary<string, ObjectDefinition>();
		private Dictionary<string, object> instances = new Dictionary<string, object>();
		private Dictionary<string, Reference> scopeReferences = new Dictionary<string, Reference>();
		private List<Action> initActions = new List<Action>();

		private static Context instance = new Context();

		public static Context Instance { get { return instance; } }

		private Context()
		{
			Initialized = false;
		}

		public void CallAfterInit(Action action)
		{
			if (Initialized)
			{
				try
				{
					action();
				}
				catch (Exception e)
				{
					Console.WriteLine(e.Message);
				}
			}
			else
			{
				initActions.Add(action);
			}
		}

		public void AddDefinition(ObjectDefinition def)
		{
			definitions[def.id] = def;
		}

		private object Instantiate(ObjectDefinition objDef)
		{
			return objDef.Instantiate(null, null);
		}

		private object InstantiateReferenced(ObjectDefinition objDef, Dictionary<string,object> localReferences)
		{
			return objDef.Instantiate(instances, localReferences);
		}

		private object Instantiate(string refId)
		{
			ObjectDefinition objDef;

			if (definitions.TryGetValue(refId, out objDef))
			{
				return Instantiate(objDef);
			}

			return null;
		}

		public IEnumerator Initialize()
		{
			if (Initialized)
			{
				yield break;
			}

			TotalInitSteps = definitions.Count * 4;

			Dictionary<string,object> localInstances = new Dictionary<string, object>();

			foreach(ObjectDefinition objDef in definitions.Values)
			{
				if (objDef.singleton && objDef.scope == GlobalScope && !instances.ContainsKey(objDef.id))
				{
					CreateObject(objDef, localInstances);
				}

				NumInitSteps++;

				yield return null;
			}

			TotalInitSteps = definitions.Count + instances.Count * 3;

			// realizar las injecciones de los objetos instanciados.
			foreach (string objRef in instances.Keys)
			{
				Inject(definitions[objRef], instances[objRef], localInstances);

				NumInitSteps++;

				yield return null;
			}

			// llamar al metodo init.
			foreach (string objRef in instances.Keys)
			{
				Init(definitions[objRef], instances[objRef]);

				NumInitSteps++;

				yield return null;
			}

			// agregar manejo de eventos
			foreach (string objRef in instances.Keys)
			{
				AddEventHandlers(definitions[objRef], instances[objRef], true);

				NumInitSteps++;

				yield return null;
			}

			Initialized = true;

			// llamar callbacks de inicializacion
			foreach (Action action in initActions)
			{
				try
				{
					action();
				}
				catch (Exception e)
				{
                    Console.WriteLine(e.Message);
				}

				yield return null;
			}

		}

		public void LoadScope(string scope, params Reference[] references)
		{
			IEnumerator loadEnum = LoadScopeSync(scope, references);

			while (loadEnum.MoveNext());
		}

		public IEnumerator LoadScopeSync(string scope, params Reference[] references)
		{
			NumLoadSteps = 0;
			TotalLoadSteps = definitions.Count * 5 + scopeReferences.Count + references.Length;

			string oldScope = CurrentScope;
			OnScopeChanging(oldScope, scope);

			currentScope = scope;

			foreach(ObjectDefinition objDef in definitions.Values)
			{
				if (objDef.scope != GlobalScope)
				{
					object obj;
					if (instances.TryGetValue(objDef.id, out obj))
					{
						// remove all subscriptions that were added before
						AddEventHandlers(objDef, obj, false);

						if (obj is IDisposable)
						{
							((IDisposable) obj).Dispose();
						}

						instances.Remove(objDef.id);

						objDef.state = ObjectCreationState.NotCreated;
					}
				}

				NumLoadSteps++;

				yield return null;
			}

			foreach (Reference reference in scopeReferences.Values)
			{
				object obj;
				if (instances.TryGetValue(reference.id, out obj))
				{
					if (obj is IDisposable)
					{
						((IDisposable) obj).Dispose();
					}

					instances.Remove(reference.id);
				}

				NumLoadSteps++;

				yield return null;
			}

			scopeReferences.Clear();

			foreach (Reference reference in references)
			{
				scopeReferences.Add(reference.id, reference);

				instances[reference.id] = reference.obj;

				NumLoadSteps++;

				yield return null;
			}

			// iterar sobre definitions y crear, injectar y llamar al init de todas las que son del scope del parametro
			Dictionary<string,object> localInstances = new Dictionary<string, object>();

			foreach(ObjectDefinition objDef in definitions.Values)
			{
				if (objDef.singleton && objDef.scope == scope && !instances.ContainsKey(objDef.id))
				{
					CreateObject(objDef, localInstances);
				}

				NumLoadSteps++;

				yield return null;
			}

			foreach (string objRef in definitions.Keys)
			{
				if (definitions[objRef].scope == scope)
				{
					Inject(definitions[objRef], instances[objRef], localInstances);
				}

				NumLoadSteps++;

				yield return null;
			}

			foreach (string objRef in definitions.Keys)
			{
				if (definitions[objRef].scope == scope)
				{
					Init(definitions[objRef], instances[objRef]);
				}

				NumLoadSteps++;

				yield return null;
			}

			foreach (string objRef in definitions.Keys)
			{
				if (definitions[objRef].scope == scope)
				{
					AddEventHandlers(definitions[objRef], instances[objRef], true);
				}

				NumLoadSteps++;

				yield return null;
			}

			OnScopeChanged(oldScope, scope);
		}

		protected void OnScopeChanging(string oldScope, string newScope)
		{
			if (ScopeChanging != null)
			{
				ScopeChanging(this, new ScopeChangeEventArgs(oldScope, newScope));
			}
		}

		protected void OnScopeChanged(string oldScope, string newScope)
		{
			if (ScopeChanged != null)
			{
				ScopeChanged(this, new ScopeChangeEventArgs(oldScope, newScope));
			}
		}

		private void Init(ObjectDefinition objDef, object target)
		{
			if (objDef.initMethodName != null)
			{
				MethodInfo method = ReflectionHelper.FindEmptyMethod(target, objDef.initMethodName);
				if (method != null)
				{
					method.Invoke(target, null);
				}
			}
		}

		private void AddEventHandlers(ObjectDefinition objDef, object target, bool add)
		{
			foreach (SubscriptionDefinition subscription in objDef.subscriptions)
			{
				foreach (PublisherDefinition publisher in subscription.publishers)
				{
					object eventPublisher;
					if (instances.TryGetValue(publisher.refId, out eventPublisher))
					{
						EventInfo eventInfo = eventPublisher.GetType().GetEvent(publisher.eventName);

                        MethodInfo handler = target.GetType().GetMethod(subscription.handler, new Type[] { typeof(object), subscription.argsType });
                        if (handler == null)
                        {
                            throw new ArgumentException( "Handler not found: check if handler method is public", "handler");
                        }

						Delegate del = Delegate.CreateDelegate(eventInfo.EventHandlerType, target, handler);

						MethodInfo methodInfoAdd = add ? eventInfo.GetAddMethod() : eventInfo.GetRemoveMethod();
						methodInfoAdd.Invoke(eventPublisher, new object[] { del });
					}
					else
					{
                        Console.WriteLine(string.Format("Cannot subscribe object {0} : publisher '{1}' not found.", objDef.id, publisher.refId));
					}
				}
			}
		}

		private void Inject(ObjectDefinition objDef, object target, Dictionary<string,object> localInstances)
		{
			foreach (PropertyDefinition propDef in objDef.properties.Values)
			{
				if (propDef.refId == null)
				{
					ReflectionHelper.SetProperty(target, propDef.name, propDef.val);
				}
				else if (definitions.ContainsKey(propDef.refId))
				{
					object val;

					instances.TryGetValue(propDef.refId, out val);
					if (val == null)
					{
						localInstances.TryGetValue(propDef.refId, out val);
					}
					if (val == null)
					{
						CreateObject(definitions[propDef.refId], localInstances);
						localInstances.TryGetValue(propDef.refId, out val);
					}

					ReflectionHelper.SetProperty(target, propDef.name, val);
				}
				else if (scopeReferences.ContainsKey(propDef.refId))
				{
					Reference reference = scopeReferences[propDef.refId];

					ReflectionHelper.SetProperty(target, propDef.name, reference.obj);
				}
				else
				{
					 Console.WriteLine(string.Format("Injecting '{0}' object, reference '{1}' not found.", objDef.id, propDef.refId));
				}
			}
		}

		private object CreateObject(ObjectDefinition objDef, Dictionary<string,object> localInstances)
		{
			objDef.state = ObjectCreationState.InProgress;
			object obj = null;
			if (objDef.ctorDef == null || objDef.ctorDef.IsByValueConstructor())
			{
				obj = objDef.Instantiate(instances, localInstances);
				if(obj != null)
				{
					if (objDef.singleton)
					{
						instances[objDef.id] = obj;
					}
					else
					{
						localInstances[objDef.id] = obj;
					}
				}
				else 
				{
					Console.WriteLine(string.Format("cant create a singleton named '{0}'.", objDef.id));
				}
			}
			else // es un constructor referenciado.
			{
				// checkear que los objetos foraneos esten creados.
				foreach (CtorParamDefinition param in objDef.ctorDef.parameters.Values)
				{
					if (param.refId != null && !instances.ContainsKey(param.refId))
					{
						ObjectDefinition foreignObj;
						if (definitions.TryGetValue(param.refId, out foreignObj))
						{
							if (foreignObj.state == ObjectCreationState.NotCreated)
							{
								object refObj = CreateObject(foreignObj,localInstances);
								if (!foreignObj.singleton)
								{
									localInstances[foreignObj.id] = refObj;
								}
							}
							else if (foreignObj.state == ObjectCreationState.InProgress)
							{
								Console.WriteLine(string.Format("deadlock creating object '{0}'.", objDef.id));
							}
						}
					}
				}
				// ahora se puede crear el objeto con referencias.
				obj = InstantiateReferenced(objDef, localInstances);
				if (obj != null)
				{
					if (objDef.singleton)
					{
						instances[objDef.id] = obj;
					}

					objDef.state = ObjectCreationState.Created;
				}
				else 
				{
					 Console.WriteLine(string.Format("cant create a singleton named '{0}'.", objDef.id));
				}
			}
			return obj;
		}

		public T GetObjectIfExists<T>(string refId, T defVal)
		{
			object singletonObj;
			if (instances.TryGetValue(refId, out singletonObj))
			{
				return (T) singletonObj;
			}

			return defVal;
		}

        public object GetObject(string refId)
        {
            object singletonObj;
            if (instances.TryGetValue(refId, out singletonObj))
            {
                return singletonObj;
            }

            return null;
        }

		public T GetObjectOrDefault<T>(string refId, T defVal)
		{
			T value = GetObjectIfExists(refId, defVal);
			if (value != null && !value.Equals(defVal))
			{
				return value;
			}

			ObjectDefinition definition;
			if (!definitions.TryGetValue(refId, out definition))
			{
				return defVal;
			}

			Dictionary<string,object> localInstances = new Dictionary<string, object>();

			object obj = CreateObject(definition, localInstances);
			Inject(definition, obj, localInstances);
			Init(definition, obj);
			AddEventHandlers(definition, obj, true);

			return (T) obj;
		}

		public T GetObject<T>(string refId)
		{
			T obj = GetObjectOrDefault<T>(refId, default (T));

			if (obj == null)
			{
				throw new KeyNotFoundException("Context refId=" + refId + " MISSING");
			}
			else
			{
				return obj;
			}
		}

		public void Dispose()
		{
			foreach (object obj in instances.Values)
			{
				if (obj is IDisposable)
				{
					((IDisposable) obj).Dispose();
				}
			}

			instances.Clear();
			definitions.Clear();
			scopeReferences.Clear();
			Initialized = false;
		}
	}

	public struct Reference
	{
		public string id;
		public object obj;

		public Reference(string id, object obj)
		{
			this.id = id;
			this.obj = obj;
		}
	}
}